﻿//-----------------------------------------------------------------------
// <copyright file="BusinessObject.cs" company="FastNET Services">
//     Copyright (c) 2007-2013. All Rights Reserved.
// </copyright>
// <warranty>
//     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
//     BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
//     DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </warranty>
//-----------------------------------------------------------------------
#region Usings

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using FastNet.BusinessSystem.Exceptions;
using FastNet.BusinessSystem.Interfaces;
using FastNet.BusinessSystem.Properties;
using FastNet.BusinessSystem.Rules;
using FastNet.RepositorySystem.Interfaces;

#endregion

namespace FastNet.BusinessSystem.Objects
{
    /// <summary>
    /// Base business object class
    /// </summary>
    /// <typeparam name="TData">IData type for the underlying data</typeparam>
    /// <typeparam name="TRepository">IRepository for managing the IData</typeparam>
    public abstract class BusinessObject<TData, TRepository> : IBusinessObject, INotifyPropertyChanged, IDataErrorInfo
        where TData : class, IData
        where TRepository : class, IRepository<TData>
    {
        #region Variables

        private readonly BusinessRuleCollection _rules;
        private readonly BusinessRuleCollection _brokenRules;

        #endregion

        #region CTor/DTor

        /// <summary>
        /// Initializes a new instance of the <see cref="BusinessObject{TData,TRepository}"/> class
        /// </summary>
        /// <param name="factory"><see cref="IRepositoryFactory"/> used for managing the data store</param>
        /// <param name="data"><typeparamref name="TData"/> reference for backing the <see cref="BusinessObject{TData,TRepository}"/></param>
        /// <exception cref="ArgumentNullException">Thrown if factory or data are null</exception>
        /// <exception cref="BusinessInitializationException">Thrown if there was an error during initialization</exception>
        protected BusinessObject(IRepositoryFactory factory, TData data)
            : this(factory, false)
        {
            if (data == null)
                throw new ArgumentNullException("data", Resources.BusinessObject_Init_DataNull);

            Data = data;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BusinessObject{TData,TRepository}"/> class
        /// </summary>
        /// <param name="factory"><see cref="IRepositoryFactory"/> used for managing the data store</param>
        /// <exception cref="BusinessInitializationException">Thrown if there was an error during initialization</exception>
        /// <exception cref="ArgumentNullException">Thrown if factory is null</exception>
        protected BusinessObject(IRepositoryFactory factory)
            : this(factory, true) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="BusinessObject{TData,TRepository}"/> class
        /// </summary>
        /// <param name="factory"><see cref="IRepositoryFactory"/> used for managing the data store</param>
        /// <param name="setNewData">Boolean whether or not to set the data up as a new object</param>
        /// <exception cref="BusinessInitializationException">Thrown if there was an error during initialization</exception>
        /// <exception cref="ArgumentNullException">Thrown if factory is null</exception>
        protected BusinessObject(IRepositoryFactory factory, bool setNewData)
        {
            if (factory == null)
                throw new ArgumentNullException("factory", Resources.BusinessObject_Init_FactoryNull);

            _rules = new BusinessRuleCollection();
            _brokenRules = new BusinessRuleCollection();

            Repository = RetrieveRepository(factory);

            if (Repository == null)
                throw new BusinessInitializationException(Resources.BusinessObject_Init_RepositoryNull);

            Factory = factory;
            HasValidated = false;
            PropertyChanged += BusinessObject_PropertyChanged;

            if (!setNewData)
            {
                Validate();
                return;
            }

            try
            {
                Data = Repository.Create();
                if (Data == null)
                    throw new BusinessException(Resources.BusinessObject_Init_DataNull);
            }
            catch (Exception e)
            {
                throw new BusinessInitializationException(Resources.BusinessObject_Init_DataError, e);
            }
        }

        #endregion

        #region Storage Properties

        /// <summary>
        /// Gets the IRepository reference for actions dealing with queries and object management
        /// </summary>
        protected TRepository Repository { get; private set; }

        /// <summary>
        /// Gets or sets the IData reference for the backing information for this business class
        /// </summary>
        protected TData Data { get; set; }

        /// <summary>
        /// Gets the IRepositoryFactory reference for the backing information for this business class
        /// </summary>
        protected IRepositoryFactory Factory { get; private set; }

        /// <summary>
        /// Gets whether or not the object has been validated yet
        /// </summary>
        protected bool HasValidated { get; private set; }

        #endregion

        #region Events

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Event raised when the object fails a validation pass
        /// </summary>
        public event EventHandler ValidationFailed;

        /// <summary>
        /// Event raised when the object is beginning a validation pass
        /// </summary>
        public event EventHandler ValidationStarted;

        /// <summary>
        /// Event raised when the object has finished its validation
        /// </summary>
        public event EventHandler ValidationEnded;

        #endregion

        #region Event Raisers

        /// <summary>
        /// Raises the event for an object property being changed
        /// </summary>
        /// <param name="e">PropertyChangedEventArgs with details about the changed property</param>
        protected void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, e);
        }

        /// <summary>
        /// Raises the event for an object being in an invalid state
        /// </summary>
        protected void OnValidationFailed()
        {
            if (ValidationFailed != null)
                ValidationFailed(this, EventArgs.Empty);
        }

        /// <summary>
        /// Raises the event for a validation that has started
        /// </summary>
        protected void OnValidationStarted()
        {
            if (ValidationStarted != null)
                ValidationStarted(this, EventArgs.Empty);
        }

        /// <summary>
        /// Raises the event for a validation that has ended 
        /// </summary>
        protected void OnValidationEnded()
        {
            if (ValidationEnded != null)
                ValidationEnded(this, EventArgs.Empty);
        }

        #endregion

        #region Handled Events

        /// <summary>
        /// Handles base logic upon a property changing
        /// </summary>
        /// <param name="sender">BusinessObject calling this method</param>
        /// <param name="e">PropertyChangedEventArgs with information on the event</param>
        // ReSharper disable InconsistentNaming
        private void BusinessObject_PropertyChanged(object sender, PropertyChangedEventArgs e)
        // ReSharper restore InconsistentNaming
        {
            HasValidated = false;
        }

        #endregion

        #region Business Validation

        /// <summary>
        /// Gets a reference to the list of business rules for validation
        /// </summary>
        protected BusinessRuleCollection BusinessRules { get { return _rules; } }

        /// <summary>
        /// Gets a list for all currently broken rules
        /// </summary>
        public IEnumerable<BusinessRule> BrokenRules { get { return _brokenRules; } }

        /// <summary>
        /// Boolean whether or not the object is currently valid
        /// </summary>
        public bool IsValid
        {
            get
            {
                if (!HasValidated) Validate();
                return _brokenRules.Count <= 0;
            }
        }

        /// <summary>
        /// Tells the business object to validate it's state and build a list of results
        /// </summary>
        /// <returns>Boolean whether or not the object passes validation</returns>
        public bool Validate()
        {
            if (HasValidated)
                return IsValid;

            OnValidationStarted();
            _brokenRules.Clear();

            foreach (BusinessRule rule in _rules.Where(rule => !rule.IsValid()))
                _brokenRules.Add(rule);
            HasValidated = true;

            if (!IsValid)
            {
                OnValidationFailed();
                OnValidationEnded();
                return false;
            }

            OnValidationEnded();
            return true;
        }

        #endregion

        #region Factory Methods

        /// <summary>
        /// Calls the repository to retrieve the instance for data access calls
        /// </summary>
        /// <param name="factory">Factory for retrieving the Repository from</param>
        /// <exception cref="ArgumentNullException">Thrown if factory is null</exception>
        /// <returns>IRepository for the Data type of this class</returns>
        protected static TRepository RetrieveRepository(IRepositoryFactory factory)
        {
            if (factory == null)
            {
                throw new ArgumentNullException("factory", Resources.BusinessObject_RetrieveRepository_FactoryNull);
            }

            return factory.GetRepository<TRepository>();
        }

        #endregion

        #region IDataErrorInfo Handlers

        /// <summary>
        /// Gets an error message indicating what is wrong with this object.
        /// </summary>
        /// <returns>
        /// An error message indicating what is wrong with this object. The default is an empty string ("").
        /// </returns>
        public string Error
        {
            get
            {
                //Update our rules first
                if (!HasValidated)
                    Validate();

                if (IsValid)
                    return string.Empty;

                StringBuilder builder = new StringBuilder();

                //Append each rule's message to the return string
                foreach (BusinessRule rule in _brokenRules)
                    builder.AppendLine(rule.ErrorMessage);

                //Remove the last new line character
                if (builder.Length > 0)
                    builder.Remove(builder.Length - Environment.NewLine.Length, Environment.NewLine.Length);

                return builder.ToString();
            }
        }

        /// <summary>
        /// Gets the error message for the property with the given name.
        /// </summary>
        /// <returns>
        /// The error message for the property. The default is an empty string ("").
        /// </returns>
        /// <param name="columnName">The name of the property whose error message to get. </param>
        public string this[string columnName]
        {
            get
            {
                //Update our rules first
                if (!HasValidated)
                    Validate();

                if (IsValid)
                    return string.Empty;

                StringBuilder builder = new StringBuilder();

                //Append each rules message to the return string
                foreach (BusinessRule rule in _brokenRules.Where(i => string.Compare(i.PropertyName, columnName, true, CultureInfo.CurrentCulture) == 0))
                    builder.AppendLine(rule.ErrorMessage);

                //Remove the last new line
                if (builder.Length > 0)
                    builder.Remove(builder.Length - Environment.NewLine.Length, Environment.NewLine.Length);

                return builder.ToString();
            }
        }

        #endregion

        #region State Properties

        /// <summary>
        /// Gets a value indicating whether or not this instance has been changed
        /// </summary>
        public bool IsDirty { get { return Data.IsDirty; } }

        /// <summary>
        /// Gets a value indicating whether or not this instance is a new object
        /// </summary>
        public bool IsNew { get { return Data.IsNew; } }

        /// <summary>
        /// Gets a value indicating whether or not this instance has been deleted
        /// </summary>
        public bool IsDeleted { get { return Data.IsDeleted; } }

        /// <summary>
        /// Gets or sets the date the object was created
        /// </summary>
        public DateTime DateCreated { get { return Data.DateCreated; } }

        /// <summary>
        /// Gets or sets the date the object was last modified
        /// </summary>
        public DateTime DateModified { get { return Data.DateModified; } }

        /// <summary>
        /// Gets or sets the date the object was deleted
        /// </summary>
        public DateTime? DateDeleted { get { return Data.DateDeleted; } }

        #endregion

        #region Implementation of IBusinessObject

        /// <summary>
        /// Gets a value indicating whether or not the current object can be modified
        /// </summary>
        public abstract bool CanModify { get; }

        /// <summary>
        /// Gets a value indicating whether or not the current object can be saved
        /// </summary>
        public abstract bool CanSave { get; }

        /// <summary>
        /// Gets a value indicating whether or not the current object can be deleted
        /// </summary>
        public abstract bool CanDelete { get; }

        #endregion

        #region State Management

        /// <summary>
        /// Persists all changes made to the data repository
        /// </summary>
        /// <exception cref="BusinessStateException">Thrown when the object is in a state that does not allow for modification</exception>
        /// <exception cref="BusinessUnchangedException">Thrown when the data is not dirty or already deleted</exception>
        /// <exception cref="BusinessValidationException">
        /// Thrown when there is a validation error with the object. Check the <see cref="BrokenRules"/> for details
        /// </exception>
        /// <exception cref="BusinessSaveException">Thrown when there was an error saving the object to the store</exception>
        public void SaveChanges()
        {
            if (!CanSave || (!Data.IsNew && !CanModify))
                throw new BusinessStateException(Resources.BusinessObject_Save_StateError);

            if ((!Data.IsDirty && !Data.IsNew) || Data.IsDeleted)
                throw new BusinessUnchangedException(Resources.BusinessObject_Save_UnchangedError);

            if (!IsValid)
                throw new BusinessValidationException(Resources.BusinessObject_Save_ValidationError);

            try
            {
                Repository.Save(Data);
            }
            catch (Exception e)
            {
                throw new BusinessSaveException(Resources.BusinessObject_Save_Error, e);
            }
        }

        /// <summary>
        /// Marks the object for deletion so it can be purged later
        /// </summary>
        /// <exception cref="BusinessStateException">Thrown when the object cannot be deleted</exception>
        /// <exception cref="BusinessUnchangedException">Thrown when the data is already deleted</exception>
        /// <exception cref="BusinessDeleteException">Thrown when there was an error deleting the object from the store</exception>
        public void Delete()
        {
            if (!CanDelete)
                throw new BusinessStateException(Resources.BusinessObject_Delete_StateError);

            if (Data.IsDeleted)
                throw new BusinessUnchangedException(Resources.BusinessObject_Delete_UnchangedError);

            try
            {
                Repository.Delete(Data);
            }
            catch (Exception e)
            {
                throw new BusinessDeleteException(Resources.BusinessObject_Delete_Error, e);
            }
        }

        #endregion
    }
}